"""
This file contains abstract base classes intended as a reference document for
creating new backend modules capable of turning a concrete, real repository
into equivalent Django model classes (see models.py).

The hierarchy loosely tracks the model hierarchy, but only so far as is useful
for encapsulating algorithms and other logic (e.g. determining the branch(es)
of a given commit, which differs from backend to backend).

A backend module must subclass every class here, and implement every method
which currently raises NotImplementedError.

To reduce verbosity we suggest naming the backend classes by replacing
"Backend" with the name of the backend, e.g. "SVNRepository" "SVNCommit" etc.
Using e.g. "SVNBackendRepository" is (in our opinion) a little too verbose.
"""

from vcs.models import Branch


class BackendRepository(object):

    def __init__(self, model_obj):
        """
        Store the model Repository instance used for finding repo URL, existing
        commits, and so forth. Unlike the other classes, BackendRepository
        expects that its model counterpart already exists, since nothing can
        be accomplished without the information stored therein.
        """
        self.model_obj = model_obj

    def update(self):
        """
        Master method that fully updates database from on-disk repository.
        """
        self.update_branches()
        self.update_tags()
        self.update_commits()

    def update_branches(self):
        """
        Adds or updates Branch objects based on current repo state.
        """
        # Set of branches currently in database
        db_branches = set(self.db_branches())
        # Set of active branches found in the repository
        cur_branches = set(self.branches())
        # Set of inactive/deleted branches found in the repository
        old_branches = set(self.old_branches())
        # Add new branches active in repo but not found in database
        for branch in cur_branches.difference(db_branches):
            branch.create_model_obj()
        # Mark all 'old' branches we know about as removed in database
        for branch in old_branches.difference(db_branches):
            # May need to create it first, if we never found it before now.
            # (I.e. branch was both made and nuked after our last update)
            if not branch.model_obj:
                branch.create_model_obj()
            branch.remove_model_obj()

    def update_tags(self):
        """
        Adds or updates Tag objects based on current repo state.
        """
        raise NotImplementedError

    def update_commits(self):
        """
        Adds or updates Commit objects based on current repo state.
        """
        # Initialize "chain" of commit parenthood
        previous = None
        for commit in self.new_commits():
            # Create db object
            model_commit = commit.create_model_obj()
            # Add branches to db object
            for branch in commit.branches():
                model_commit.branches.add(branch.model_obj)
            # Add parent(s) to db object
            # TODO: handle merges
            if previous:
                # Add this commit as parent of previous
                model_commit.parents.add(previous.model_obj)
                # And remove previous' head status (we're the new HEAD)
                previous.model_obj.is_head=False
                previous.model_obj.save()
            previous = commit
            # Update/create FileModification objects
            commit.update_file_modifications()
            # TODO: remove old commits? (e.g. if someone manually modified the
            # repo to remove some commits)



    def new_commits(self):
        """
        Return all BackendCommit objects which have not been imported into our
        repository yet (and thus do not have corresponding Commit objects).
        """
        raise NotImplementedError

    def db_branches(self):
        """
        Return a list of BackendBranch objects representing the currently known
        (in-database) branches.
        """
        raise NotImplementedError

    def branches(self):
        """
        Return all BackendBranches reachable from repository HEAD.
        """
        raise NotImplementedError

    def old_branches(self):
        """
        Return all BackendBranches no longer reachable from HEAD. Only applies
        to SVN and similar systems without a baked-in concept of branches.
        """
        raise NotImplementedError

    def tags(self):
        """
        Return all BackendTags reachable from repository HEAD.
        """
        raise NotImplementedError

    def old_tags(self):
        """
        Return all BackendTags no longer reachable from HEAD. Only applies
        to SVN and similar systems without a baked-in concept of tags.
        """
        raise NotImplementedError

    def is_empty(self):
        """
        Attempts to determine if this repository is a special "empty" edge case
        (i.e. an SVN or Git repo with zero commits) which is used in a few spots
        to prevent outright errors when the edge case comes up.
        """
        raise NotImplementedError


class BackendBranch(object):
    
    def __init__(self, repo, name):
        """
        Store pointer to containing BackendRepository, and name. Also set
        model_obj to our model Branch, or None if there isn't one yet.
        """
        self.repo = repo
        self.name = name
        try:
            self.model_obj = self.repo.model_obj.branches.get(name=self.name)
        except Branch.DoesNotExist:
            self.model_obj = None

    def __eq__(self, other):
        """
        Define equality testing based on name to allow use in sets.
        """
        return other.name == self.name

    def create_model_obj(self):
        """
        Create and return a model Branch corresponding to this object.
        """
        self.model_obj = Branch.objects.create(
            repository=self.repo.model_obj,
            name=self.name,
            path=self.path(),
        )
        return self.model_obj

    def remove_model_obj(self):
        """
        Find our corresponding model Branch and mark it as removed.
        """
        self.model_obj.removed = True
        self.model_obj.save()


class BackendTag(object):

    def __init__(self, branch, name):
        """
        Store pointer to the BackendBranch we were cut from, and name. Also 
        attach corresponding model object if it exists.
        """
        self.branch = branch
        self.name = name
        try:
            self.model_obj = self.branch.model_obj.tags.get(name=name)
        except Tag.DoesNotExist:
            self.model_obj = None

    def __eq__(self, other):
        """
        Define equality testing based on name to allow use in sets.
        """
        return other.name == self.name
    
    def create_model_obj(self):
        """
        Create Tag object corresponding to this object, then return it.
        """
        self.model_obj = self.branch.model_obj.tags.create(
            name=self.name
        )
        return self.model_obj


class BackendCommit(object):

    def __init__(self, branches, identifier):
        """
        Set our branch list and identifier. Since many operations hinge on
        obtaining branches[0], do a quick sanity check.
        """
        if not len(branches):
            raise ValueError, "Must specify at least one branch!"
        self.branches = branches
        self.identifier = identifier

    def parents(self):
        """
        Returns the BackendCommit objects immediately preceding this commit.
        """
        raise NotImplementedError

    def create_model_obj(self):
        """
        Create Commit object corresponding to this object, then return it.
        """
        self.model_obj = self.branches[0].model_obj.commits.create(
            identifier=self.identifier
        )
        return self.model_obj

    def update_file_modifications(self):
        """
        Create or update FileModification objects associated with this commit.
        """
        raise NotImplementedError
